// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/proxy/proxy_list.h"

#include <vector>

#include "net/base/net_errors.h"
#include "net/log/net_log.h"
#include "net/proxy/proxy_retry_info.h"
#include "net/proxy/proxy_server.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace net {

namespace {

    // Test parsing from a PAC string.
    TEST(ProxyListTest, SetFromPacString)
    {
        const struct {
            const char* pac_input;
            const char* pac_output;
        } tests[] = {
            // Valid inputs:
            {
                "PROXY foopy:10",
                "PROXY foopy:10",
            },
            {
                " DIRECT", // leading space.
                "DIRECT",
            },
            {
                "PROXY foopy1 ; proxy foopy2;\t DIRECT",
                "PROXY foopy1:80;PROXY foopy2:80;DIRECT",
            },
            {
                "proxy foopy1 ; SOCKS foopy2",
                "PROXY foopy1:80;SOCKS foopy2:1080",
            },
            // Try putting DIRECT first.
            {
                "DIRECT ; proxy foopy1 ; DIRECT ; SOCKS5 foopy2;DIRECT ",
                "DIRECT;PROXY foopy1:80;DIRECT;SOCKS5 foopy2:1080;DIRECT",
            },
            // Try putting DIRECT consecutively.
            {
                "DIRECT ; proxy foopy1:80; DIRECT ; DIRECT",
                "DIRECT;PROXY foopy1:80;DIRECT;DIRECT",
            },

            // Invalid inputs (parts which aren't understood get
            // silently discarded):
            //
            // If the proxy list string parsed to empty, automatically fall-back to
            // DIRECT.
            {
                "PROXY-foopy:10",
                "DIRECT",
            },
            {
                "PROXY",
                "DIRECT",
            },
            {
                "PROXY foopy1 ; JUNK ; JUNK ; SOCKS5 foopy2 ; ;",
                "PROXY foopy1:80;SOCKS5 foopy2:1080",
            },
        };

        for (size_t i = 0; i < arraysize(tests); ++i) {
            ProxyList list;
            list.SetFromPacString(tests[i].pac_input);
            EXPECT_EQ(tests[i].pac_output, list.ToPacString());
            EXPECT_FALSE(list.IsEmpty());
        }
    }

    TEST(ProxyListTest, RemoveProxiesWithoutScheme)
    {
        const struct {
            const char* pac_input;
            int filter;
            const char* filtered_pac_output;
        } tests[] = {
            {
                "PROXY foopy:10 ; SOCKS5 foopy2 ; SOCKS foopy11 ; PROXY foopy3 ; DIRECT",
                // Remove anything that isn't HTTP or DIRECT.
                ProxyServer::SCHEME_DIRECT | ProxyServer::SCHEME_HTTP,
                "PROXY foopy:10;PROXY foopy3:80;DIRECT",
            },
            {
                "PROXY foopy:10 ; SOCKS5 foopy2",
                // Remove anything that isn't HTTP or SOCKS5.
                ProxyServer::SCHEME_DIRECT | ProxyServer::SCHEME_SOCKS4,
                "",
            },
        };

        for (size_t i = 0; i < arraysize(tests); ++i) {
            ProxyList list;
            list.SetFromPacString(tests[i].pac_input);
            list.RemoveProxiesWithoutScheme(tests[i].filter);
            EXPECT_EQ(tests[i].filtered_pac_output, list.ToPacString());
        }
    }

    TEST(ProxyListTest, DeprioritizeBadProxies)
    {
        // Retry info that marks a proxy as being bad for a *very* long time (to avoid
        // the test depending on the current time.)
        ProxyRetryInfo proxy_retry_info;
        proxy_retry_info.bad_until = base::TimeTicks::Now() + base::TimeDelta::FromDays(1);

        // Call DeprioritizeBadProxies with an empty map -- should have no effect.
        {
            ProxyList list;
            list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

            ProxyRetryInfoMap retry_info_map;
            list.DeprioritizeBadProxies(retry_info_map);
            EXPECT_EQ("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80",
                list.ToPacString());
        }

        // Call DeprioritizeBadProxies with 2 of the three proxies marked as bad.
        // These proxies should be retried last.
        {
            ProxyList list;
            list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

            ProxyRetryInfoMap retry_info_map;
            retry_info_map["foopy1:80"] = proxy_retry_info;
            retry_info_map["foopy3:80"] = proxy_retry_info;
            retry_info_map["socks5://localhost:1080"] = proxy_retry_info;

            list.DeprioritizeBadProxies(retry_info_map);

            EXPECT_EQ("PROXY foopy2:80;PROXY foopy1:80;PROXY foopy3:80",
                list.ToPacString());
        }

        // Call DeprioritizeBadProxies where ALL of the proxies are marked as bad.
        // This should have no effect on the order.
        {
            ProxyList list;
            list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

            ProxyRetryInfoMap retry_info_map;
            retry_info_map["foopy1:80"] = proxy_retry_info;
            retry_info_map["foopy2:80"] = proxy_retry_info;
            retry_info_map["foopy3:80"] = proxy_retry_info;

            list.DeprioritizeBadProxies(retry_info_map);

            EXPECT_EQ("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80",
                list.ToPacString());
        }

        // Call DeprioritizeBadProxies with 2 of the three proxies marked as bad. Of
        // the 2 bad proxies, one is to be reconsidered and should be retried last.
        // The other is not to be reconsidered and should be removed from the list.
        {
            ProxyList list;
            list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

            ProxyRetryInfoMap retry_info_map;
            // |proxy_retry_info.reconsider defaults to true.
            retry_info_map["foopy1:80"] = proxy_retry_info;
            proxy_retry_info.try_while_bad = false;
            retry_info_map["foopy3:80"] = proxy_retry_info;
            proxy_retry_info.try_while_bad = true;
            retry_info_map["socks5://localhost:1080"] = proxy_retry_info;

            list.DeprioritizeBadProxies(retry_info_map);

            EXPECT_EQ("PROXY foopy2:80;PROXY foopy1:80",
                list.ToPacString());
        }
    }

    TEST(ProxyListTest, UpdateRetryInfoOnFallback)
    {
        ProxyRetryInfo proxy_retry_info;
        // Retrying should put the first proxy on the retry list.
        {
            ProxyList list;
            ProxyRetryInfoMap retry_info_map;
            BoundNetLog net_log;
            ProxyServer proxy_server(
                ProxyServer::FromURI("foopy1:80", ProxyServer::SCHEME_HTTP));
            std::vector<ProxyServer> bad_proxies;
            bad_proxies.push_back(proxy_server);
            list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");
            list.UpdateRetryInfoOnFallback(
                &retry_info_map, base::TimeDelta::FromSeconds(60), true, bad_proxies,
                ERR_PROXY_CONNECTION_FAILED, net_log);
            EXPECT_TRUE(retry_info_map.end() != retry_info_map.find("foopy1:80"));
            EXPECT_EQ(ERR_PROXY_CONNECTION_FAILED,
                retry_info_map[proxy_server.ToURI()].net_error);
            EXPECT_TRUE(retry_info_map.end() == retry_info_map.find("foopy2:80"));
            EXPECT_TRUE(retry_info_map.end() == retry_info_map.find("foopy3:80"));
        }
        // Retrying should put the first proxy on the retry list, even if there
        // was no network error.
        {
            ProxyList list;
            ProxyRetryInfoMap retry_info_map;
            BoundNetLog net_log;
            ProxyServer proxy_server(
                ProxyServer::FromURI("foopy1:80", ProxyServer::SCHEME_HTTP));
            std::vector<ProxyServer> bad_proxies;
            bad_proxies.push_back(proxy_server);
            list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");
            list.UpdateRetryInfoOnFallback(&retry_info_map,
                base::TimeDelta::FromSeconds(60), true,
                bad_proxies, OK, net_log);
            EXPECT_TRUE(retry_info_map.end() != retry_info_map.find("foopy1:80"));
            EXPECT_EQ(OK, retry_info_map[proxy_server.ToURI()].net_error);
            EXPECT_TRUE(retry_info_map.end() == retry_info_map.find("foopy2:80"));
            EXPECT_TRUE(retry_info_map.end() == retry_info_map.find("foopy3:80"));
        }
        // Including another bad proxy should put both the first and the specified
        // proxy on the retry list.
        {
            ProxyList list;
            ProxyRetryInfoMap retry_info_map;
            BoundNetLog net_log;
            ProxyServer proxy_server = ProxyServer::FromURI("foopy3:80",
                ProxyServer::SCHEME_HTTP);
            std::vector<ProxyServer> bad_proxies;
            bad_proxies.push_back(proxy_server);
            list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");
            list.UpdateRetryInfoOnFallback(
                &retry_info_map, base::TimeDelta::FromSeconds(60), true, bad_proxies,
                ERR_NAME_RESOLUTION_FAILED, net_log);
            EXPECT_TRUE(retry_info_map.end() != retry_info_map.find("foopy1:80"));
            EXPECT_EQ(ERR_NAME_RESOLUTION_FAILED,
                retry_info_map[proxy_server.ToURI()].net_error);
            EXPECT_TRUE(retry_info_map.end() == retry_info_map.find("foopy2:80"));
            EXPECT_TRUE(retry_info_map.end() != retry_info_map.find("foopy3:80"));
        }
        // If the first proxy is DIRECT, nothing is added to the retry list, even
        // if another bad proxy is specified.
        {
            ProxyList list;
            ProxyRetryInfoMap retry_info_map;
            BoundNetLog net_log;
            ProxyServer proxy_server = ProxyServer::FromURI("foopy2:80",
                ProxyServer::SCHEME_HTTP);
            std::vector<ProxyServer> bad_proxies;
            bad_proxies.push_back(proxy_server);
            list.SetFromPacString("DIRECT;PROXY foopy2:80;PROXY foopy3:80");
            list.UpdateRetryInfoOnFallback(&retry_info_map,
                base::TimeDelta::FromSeconds(60), true,
                bad_proxies, OK, net_log);
            EXPECT_TRUE(retry_info_map.end() == retry_info_map.find("foopy2:80"));
            EXPECT_TRUE(retry_info_map.end() == retry_info_map.find("foopy3:80"));
        }
        // If the bad proxy is already on the retry list, and the old retry info would
        // cause the proxy to be retried later than the newly specified retry info,
        // then the old retry info should be kept.
        {
            ProxyList list;
            ProxyRetryInfoMap retry_info_map;
            BoundNetLog net_log;
            list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

            // First, mark the proxy as bad for 60 seconds.
            list.UpdateRetryInfoOnFallback(
                &retry_info_map, base::TimeDelta::FromSeconds(60), true,
                std::vector<ProxyServer>(), ERR_PROXY_CONNECTION_FAILED, net_log);
            // Next, mark the same proxy as bad for 1 second. This call should have no
            // effect, since this would cause the bad proxy to be retried sooner than
            // the existing retry info.
            list.UpdateRetryInfoOnFallback(&retry_info_map,
                base::TimeDelta::FromSeconds(1), false,
                std::vector<ProxyServer>(), OK, net_log);
            EXPECT_TRUE(retry_info_map.end() != retry_info_map.find("foopy1:80"));
            EXPECT_EQ(ERR_PROXY_CONNECTION_FAILED,
                retry_info_map["foopy1:80"].net_error);
            EXPECT_TRUE(retry_info_map["foopy1:80"].try_while_bad);
            EXPECT_EQ(base::TimeDelta::FromSeconds(60),
                retry_info_map["foopy1:80"].current_delay);
            EXPECT_GT(retry_info_map["foopy1:80"].bad_until,
                base::TimeTicks::Now() + base::TimeDelta::FromSeconds(30));
            EXPECT_TRUE(retry_info_map.end() == retry_info_map.find("foopy2:80"));
            EXPECT_TRUE(retry_info_map.end() == retry_info_map.find("foopy3:80"));
        }
        // If the bad proxy is already on the retry list, and the newly specified
        // retry info would cause the proxy to be retried later than the old retry
        // info, then the old retry info should be replaced with the new retry info.
        {
            ProxyList list;
            ProxyRetryInfoMap retry_info_map;
            BoundNetLog net_log;
            list.SetFromPacString("PROXY foopy1:80;PROXY foopy2:80;PROXY foopy3:80");

            // First, mark the proxy as bad for 1 second.
            list.UpdateRetryInfoOnFallback(&retry_info_map,
                base::TimeDelta::FromSeconds(1), false,
                std::vector<ProxyServer>(), OK, net_log);
            // Next, mark the same proxy as bad for 60 seconds. This call should replace
            // the existing retry info with the new 60 second retry info.
            list.UpdateRetryInfoOnFallback(
                &retry_info_map, base::TimeDelta::FromSeconds(60), true,
                std::vector<ProxyServer>(), ERR_PROXY_CONNECTION_FAILED, net_log);

            EXPECT_TRUE(retry_info_map.end() != retry_info_map.find("foopy1:80"));
            EXPECT_EQ(ERR_PROXY_CONNECTION_FAILED,
                retry_info_map["foopy1:80"].net_error);
            EXPECT_TRUE(retry_info_map["foopy1:80"].try_while_bad);
            EXPECT_EQ(base::TimeDelta::FromSeconds(60),
                retry_info_map["foopy1:80"].current_delay);
            EXPECT_GT(retry_info_map["foopy1:80"].bad_until,
                base::TimeTicks::Now() + base::TimeDelta::FromSeconds(30));
            EXPECT_TRUE(retry_info_map.end() == retry_info_map.find("foopy2:80"));
            EXPECT_TRUE(retry_info_map.end() == retry_info_map.find("foopy3:80"));
        }
    }

} // namesapce

} // namespace net
